//! Support functions for system calls that involve file descriptors.
const File = @This();

/// reference count
ref: usize = 0,
readable: bool = false,
writable: bool = false,

data: Data = .none,

const Type = enum(u8) { none, pipe, file, device };
const Data = union(Type) {
    none,
    pipe: *Pipe,
    file: struct {
        inode: *Inode,
        offset: fs.off_t = 0,
    },
    device: struct {
        inode: *Inode,
        major: u16 = 0,
    },
};

var lock: os.Lock.Spin = .init("files");
var files: [os.NFILE]File = @splat(.{});

/// Allocate a file structure.
pub fn alloc() error{SystemFdQuotaExceeded}!*File {
    lock.acquire();
    defer lock.release();

    for (&files) |*file| {
        if (file.ref == 0) {
            file.ref = 1;
            return file;
        }
    }

    return error.SystemFdQuotaExceeded;
}

/// Increment ref count for file.
pub fn dup(file: *File) *File {
    lock.acquire();
    defer lock.release();

    if (file.ref < 1) @panic("filedup");

    file.ref += 1;
    return file;
}

// Close file.  (Decrement ref count, close when reaches 0.)
pub fn close(self: *File) void {
    const data: Data, const writable = f: {
        lock.acquire();
        defer lock.release();

        if (self.ref < 1) @panic("fileclose");

        self.ref -= 1;
        if (self.ref > 0) return;

        defer self.ref = 0;
        defer self.data = .none;
        break :f .{ self.data, self.writable };
    };

    switch (data) {
        .none => {},
        .pipe => |p| p.close(writable),
        inline .file, .device => |*f| {
            log.begin();
            defer log.end();
            f.inode.put();
        },
    }
}

// Get metadata about file f.
pub fn stat(self: *File) !fs.Inode.Stat {
    switch (self.data) {
        .none, .pipe => return error.AccessDenied,
        inline .file, .device => |*f| {
            f.inode.lock();
            defer f.inode.unlock();
            return f.inode.stat();
        },
    }
}

pub fn seek(self: *File, offset: fs.off_t, whence: fs.SeekMode) !u32 {
    switch (self.data) {
        .file => |*f| {
            const signed_off: std.meta.Int(.signed, @bitSizeOf(fs.off_t)) = @bitCast(offset);

            switch (whence) {
                .set => f.offset = offset,
                .cur => f.offset = try os.lib.intCast(fs.off_t, @as(i64, f.offset) + signed_off),
                .end => {
                    f.inode.lock();
                    defer f.inode.unlock();
                    f.offset = try os.lib.intCast(fs.off_t, @as(i64, f.inode.disk.size) + signed_off);
                },
            }
            return f.offset;
        },
        else => return error.AccessDenied,
    }
}

// Read from file f.
pub fn read(self: *File, addr: usize, len: usize) !usize {
    if (!self.readable) return error.NotOpenForReading;

    switch (self.data) {
        .none => @panic("invalid file type"),
        .pipe => |p| return p.read(addr, len),
        .device => |*dev| {
            if (dev.major < 0 or dev.major >= os.NDEV)
                return error.NoDevice;

            const read_fn = fs.devsw[dev.major].read orelse return error.NoDevice;
            return read_fn(addr, len) catch error.ReadFailed;
        },
        .file => |*f| {
            const large = len > fs.BSIZE * 2;
            const buffer: []u8 = if (large) try gpa.alloc(u8, os.PGSIZE) else &.{};
            defer if (large) gpa.free(buffer);

            const p = os.proc.current().?;
            var writer = p.writer(.auto, buffer, @ptrFromInt(addr));

            f.inode.lock();
            defer f.inode.unlock();

            const size = try f.inode.read(&writer.io_w, f.offset, len);
            f.offset += @intCast(size);
            return size;
        },
    }
}

/// Write to file f.
pub fn write(self: *File, addr: usize, len: usize) !usize {
    if (!self.writable) return error.NotOpenForWriting;

    switch (self.data) {
        .none => @panic("invalid file type"),
        .pipe => |p| return p.write(addr, len),
        .device => |*dev| {
            if (dev.major < 0 or dev.major >= os.NDEV)
                return error.NoDevice;

            const write_fn = fs.devsw[dev.major].write orelse return error.NoDevice;
            return write_fn(addr, len) catch error.WriteFailed;
        },
        .file => |*f| {
            // write a few blocks at a time to avoid exceeding the maximum log transaction size,
            // including i-node, indirect block, allocation blocks, and 2 blocks of slop for non-aligned
            // writes.
            const max = ((fs.MAXOPBLOCKS - 1 - 1 - 2) / 2) * fs.BSIZE;

            const large = len > fs.BSIZE * 2;
            const buffer: []u8 = if (large) try gpa.alloc(u8, os.PGSIZE) else &.{};
            defer if (large) gpa.free(buffer);

            const p = os.proc.current().?;
            var reader = p.reader(.auto, buffer, @ptrFromInt(addr));

            var i: usize = 0;
            var nbytes: usize = 0;
            while (i < len) : (i += @intCast(nbytes)) {
                log.begin();
                defer log.end();

                f.inode.lock();
                defer f.inode.unlock();

                nbytes = @min(max, len - i);
                try f.inode.write(&reader.io_r, f.offset, nbytes);
                f.offset += @intCast(nbytes);
            }

            return len;
        },
    }
}

const fs = @import("../fs.zig");
const os = @import("../os.zig");
const log = @import("log.zig");
const Pipe = @import("Pipe.zig");
const Inode = @import("Inode.zig");
const std = @import("std");
const gpa = std.heap.page_allocator;
